package peer.NetworkManager;


import java.util.*;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.*;
import java.io.*;

import javax.management.Query;

import common.*;
import peer.SearchManager.*;
import peer.SharingManager.*;
import peer.gui.PeerGUI;
/**
 * NetworkManager receives and sends packets, processes the packet and deligates it for appropriate use
 * Also contains a collection of packets awaiting a reply before they can be processed.
 * A Singleton class
 * @author Bianca Rinaldi
 * 
 */
public class NetworkManager implements Runnable
{
	//contains the refernce to the NetworkManger object
	private static NetworkManager Singleton;
	private static PeerGUI PeerGui = PeerGUI.instance(); 
	//server address and port details
	private String SERVER_ADDRESS;
	private static final int SERVER_SOCKET_PORT = 10240;
	
	private static Queue<DatagramPacket> PacketList = new ArrayDeque<DatagramPacket>();
	private PacketFactory packetFactory = new PacketFactory();
	
	private InetAddress serverAddress;
	private InetSocketAddress serverSocket;
	private DatagramSocket QuerySocket;
	private Socket TransferSocket;
	
	private ArrayList<InetSocketAddress> peerList = new ArrayList<InetSocketAddress>();
	
	private SharingManager sharingManager;
	private SearchManager searchManager;
	
	private NetworkManager() throws Exception
	{
		setSockets();
		sharingManager = SharingManager.instance();
		searchManager = SearchManager.instance();
	}
	
	//checks that an instance of the NetworkManager class has been constructed, allows only one instance
	public static NetworkManager instance() 
	{
		try
		{
			if(Singleton == null)
			{
				Singleton = new NetworkManager();
			}
			return Singleton;
		}
		catch (Exception e)
		{
			e.printStackTrace();
			throw new Error("Could not create Network Manager!");
		}
	}
	
	public void connectToNetwork(String serverIP)
	{
	
		try {
			SERVER_ADDRESS = serverIP;
			Packet data = packetFactory.requestConnection(serverAddress, false);
			byte[] rawData = data.toString().getBytes();
			DatagramPacket toSend = new DatagramPacket(rawData, rawData.length, InetAddress.getByName(SERVER_ADDRESS), SERVER_SOCKET_PORT);
			QuerySocket.send(toSend);
			PeerGui.writeLog("Connected To Server " + SERVER_ADDRESS);
		} catch (IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}
	
	/*public void aokNetworkConnection()
	{
		try {
			Packet data = packetFactory.requestConnection(serverAddress, true);
			byte[] rawData = data.toString().getBytes();
			DatagramPacket toSend = new DatagramPacket(rawData, rawData.length, InetAddress.getByName(SERVER_ADDRESS), SERVER_SOCKET_PORT);
			QuerySocket.send(toSend);
			PeerGui.writeLog("Ack server " + SERVER_ADDRESS);
		} catch (IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}*/
	
	public void imAlive()
	{
		try {
			Packet data = packetFactory.aliveResponse(serverAddress);
			byte[] rawData = data.toString().getBytes();
			DatagramPacket toSend = new DatagramPacket(rawData, rawData.length, InetAddress.getByName(SERVER_ADDRESS), SERVER_SOCKET_PORT);
			QuerySocket.send(toSend);
			PeerGui.writeLog("Sent PONG respose to Server" + SERVER_ADDRESS);
		} catch (IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}
	
	public void requestPeerList()
	{
		try {
			Packet data = packetFactory.requestPeerList(serverAddress, false);
			byte[] rawData = data.toString().getBytes();
			DatagramPacket toSend = new DatagramPacket(rawData, rawData.length, InetAddress.getByName(SERVER_ADDRESS), SERVER_SOCKET_PORT);
			QuerySocket.send(toSend);
			PeerGui.writeLog("Request Peer List from Server " + SERVER_ADDRESS);
		} catch (IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}
	
	private void processPacket(Packet received, InetSocketAddress address)
	{
	//	PeerGui.writeLog("Recieved A packet");
		switch(received.getType())
		{
		case 0x02://0000 0010 - nw_connect_accept
		case 0x04://0000 0100 - nw_ping
		case 0x06://0000 0110 - nw_pong
		case 0x07://0000 0111 - nw_aok
		case 0x16://0001 0110 - found_response
		case 0x17://0001 0111 - found_response_aok
		case 0x1B://0001 1011 - request_file_response_aok
		case 0x1F://0001 1111 - status_response_aok
		case 0x2E://00101110 - nw_newpeers_accept
			processInternally(received);
			break;
		case 0x10://0001 0000 - search
		case 0x18://0001 1000 - request_file
		case 0x1C://0001 1100 - status_request
		case 0x24://0010 0100 - request_block
		case 0x28://0010 1000 - data
			sendtoSharingManager(received, address);
			break;
		case 0x14://0001 0100 - found
		case 0x1A://0001 1010 - request_file_response
		case 0x1E://0001 1110 - status_response
			sendtoSearchManager(received, address);
			break;
		//default is to drop the packet
		default:
			PeerGui.writeLog("Did nothing");
			break;
		}
	}
	
	private void sendtoSharingManager(Packet received, InetSocketAddress fromAddress) 
	{
		if(received.isType(Packet.Type.search))
		{
			
		}
		if(received.isType(Packet.Type.request_block))//if a ping is received from the server
		{
			PeerGui.writeLog("recieved 'request_block' for block " + received.getHeader("block") + " of " + received.getHeader("name") + " from " + fromAddress.getAddress().getHostAddress() + ":" + fromAddress.getPort());
		}
		if(received.isType(Packet.Type.data))//if a ping is received from the server
		{
			PeerGui.writeLog("recieved 'data' for block " + received.getHeader("block") + " of " + received.getHeader("name") + " from " + fromAddress.getAddress().getHostAddress() + ":" + fromAddress.getPort());
		}
		sharingManager.getPacket(received, fromAddress);
	}

	private void sendtoSearchManager(Packet received, InetSocketAddress fromAddress) 
	{
		if(received.isType(Packet.Type.found))
		{
			PeerGui.writeLog("recieved 'found' for " + received.getHeader("query") + " from " + fromAddress.getAddress().getHostAddress() + ":" + fromAddress.getPort());
			PeerGui.addSearchResult(received.getHeader("query"), received.getHeader("name"), Long.parseLong(received.getHeader("file_size")), received.getHeader("hash_code"));
		
		}
		if(received.isType(Packet.Type.request_file_response))//if a ping is received from the server
		{
			PeerGui.writeLog("recieved 'request_file_response' for " + received.getHeader("name") + " from " + fromAddress.getAddress().getHostAddress() + ":" + fromAddress.getPort());
			
		}
		if(received.isType(Packet.Type.status_response))//if a ping is received from the server
		{
			PeerGui.writeLog("recieved 'status_response' for " + received.getHeader("name") + " from " + fromAddress.getAddress().getHostAddress() + ":" + fromAddress.getPort());
			
		}
		searchManager.getPacket(received, fromAddress);
	}

	private void processInternally(Packet received)
	{
		if(received.isType(Packet.Type.nw_connect_accept))
		{
			//send an im alive packet as a response
			PeerGui.writeLog("Recieved Connection accept");
			PeerGui.setConnectionStatus("Connected");
			//aokNetworkConnection();
		}
		if(received.isType(Packet.Type.nw_ping))//if a ping is received from the server
		{
			//send an im alive packet as a response
			PeerGui.writeLog("Recieved Ping from Server");
			imAlive();
		}
		if(received.isType(Packet.Type.nw_newpeers_accept))//if a ping is received from the server
		{
			//send an im alive packet as a response
			PeerGui.writeLog("Recieved peer list from Server");
			extractPeerList(received);
		}
		
	}
	public void sendPeerPacket(Packet toSend, InetSocketAddress destination)
	{
		PeerGui.writeLog("sending packet: " + toSend.getType() + " to " + destination.getAddress().getHostAddress() + ":" + destination.getPort());
		PeerGui.writeLog(toSend.toString());
		byte[] bytes = toSend.getBytes();
		DatagramPacket dp = new DatagramPacket(bytes, bytes.length);
		dp.setSocketAddress(destination);
		
		try
		{
			QuerySocket.send(dp);
		} catch (IOException e)
		{
			System.err.println("error sending packet");
		}
		
	}

	public void sendPacket(Packet toSend) throws IOException
	{
		//PeerGui.writeLog("sending packet: " + toSend.getType() + " to " + destinations.getHostAddress());
		DatagramPacket dp = new DatagramPacket(toSend.toString().getBytes(), toSend.toString().getBytes().length);
		dp.setAddress(serverAddress);
		dp.setPort(10240);
		
		QuerySocket.send(dp);
	}
	
	//takes the peer list response packet and retrieves all the peerList data and creates a peerList of InetSocketAddresses
	public void extractPeerList(Packet peerListPacket)
	{
		PeerGui.addNewPeers(peerListPacket.getHeader("peer_list"));
		
		String addresses = peerListPacket.getHeader("peer_list");		
		if(!addresses.equals(""))
		{
			String[] array = addresses.split(";");
			for (String address : array)
			{
				String[] split = address.split(":");
				peerList.add(new InetSocketAddress(split[0], Integer.parseInt(split[1]))); 
			}
		}
	}
	
	public void sendPacket(Packet toSend, List<InetSocketAddress> destinations) throws IOException
	{
		DatagramPacket dp = new DatagramPacket(toSend.toString().getBytes(), toSend.toString().getBytes().length);
		for(InetSocketAddress d: destinations)
		{
			QuerySocket.connect(d);
			QuerySocket.send(dp);
			QuerySocket.disconnect();
		}
	}
	
	public void sendData(Packet toSend, String ipAddress, int socketNumber)
	{
		try 
		{
			Socket sendSocket = new Socket(ipAddress, socketNumber);
			DataOutputStream send = new DataOutputStream(sendSocket.getOutputStream());
			send.writeBytes(toSend.toString());
			send.close();
		} 
		catch (UnknownHostException e) 
		{
			// TODO Auto-generated catch block
			e.printStackTrace();
		} catch (IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}
	
	public void setSockets() throws Exception
	{
		serverAddress = InetAddress.getByName(SERVER_ADDRESS);
		QuerySocket = new DatagramSocket();
		TransferSocket = new Socket();
	}
	
	

	public void run() 
	{
		//waits for a packet to be receieved, creates Datagram packet of packet received and sends to be processed
		while(true)
		{
			byte[] inputBytes = new byte[1024];
			DatagramPacket receivedPacket = new DatagramPacket(inputBytes, inputBytes.length);
			try {
				//System.out.println(InetAddress.getLocalHost().getHostAddress() + ":" + QuerySocket.getLocalPort() + " listening");
				QuerySocket.receive(receivedPacket);
			} catch (IOException e)
			{
				e.printStackTrace();
			}
			Packet p = new Packet(inputBytes);
			
			InetSocketAddress fromAddress = new InetSocketAddress(receivedPacket.getAddress(), receivedPacket.getPort());
			PeerGui.writeLog("received packet: " + p.getType() + " from " + fromAddress.getAddress().getHostAddress() + ":" + fromAddress.getPort());
			PeerGui.writeLog(p.toString());
			processPacket(p, fromAddress);
		}
		
	}
	
	public void processTransferPacket(Packet p, InetSocketAddress fromAddress)
	{
		processPacket(p, fromAddress);
	}

	public ArrayList<InetSocketAddress> getPeerList()
	{
		return peerList;
	}

	public DatagramSocket getQuerySocket()
	{
		return QuerySocket;
	}
	
}
